package com.ruoyi.framework.mybatis;

import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.reflect.ReflectUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.shiro.service.SysLoginService;
import com.ruoyi.system.service.ISysUserService;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SelectItemVisitorAdapter;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;

import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

import static com.ruoyi.common.constant.DataScopeConstants.*;

/**
 * mybatis拦截器  拦截sql的执行
 */
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public class DataScopeInterceptors implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(DataScopeInterceptors.class);

    /**
     * 进行拦截的时候要执行的方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            // select 语句

            //由于逻辑关系，只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = mappedStatement.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }

            //id为执行的mapper方法的全路径名，如com.metro.dao.UserMapper.insertUser
            String id = mappedStatement.getId();
            //获取到原始sql语句
            String sql = boundSql.getSql();
            //----------------------------------------------------
            String[] skipStatementList = null;
            String[] allowStatementList = null;
            try {
                skipStatementList = Files.readAllLines(ResourceUtils.getFile("classpath:skipstatement.txt").toPath()).toArray(new String[0]);
                allowStatementList = Files.readAllLines(ResourceUtils.getFile("classpath:allowstatement.txt").toPath()).toArray(new String[0]);
            } catch (FileNotFoundException e) {
                logger.warn("has error, {}", e.getMessage());
            }
            if (mappedStatement.getSqlCommandType() != SqlCommandType.SELECT || StringUtils.containsAnyIgnoreCase(id, skipStatementList)) {
                return invocation.proceed();
            }
            if (!StringUtils.containsAnyIgnoreCase(id, allowStatementList)) {
                return invocation.proceed();
            }
            // select 语句, 表名, 拼接sql
            SysUser currentUser = ShiroUtils.getSysUser();
            // 当前角色可能发生改变
            PageUtils.clearPage();
            currentUser = SpringUtils.getBean(ISysUserService.class).selectUserById(currentUser.getUserId());
            SpringUtils.getBean(SysLoginService.class).setRolePermission(currentUser);
            PageUtils.startPage();

            if (currentUser == null || currentUser.isAdmin()) {
                return invocation.proceed();
            }
            // 排除 用户 , 角色不可用, 用户在登录的时候排除了， 角色的菜单权限的额时候排除了
            Statement statement = CCJSqlParserUtil.parse(sql);
            //  column, left join, where 拼接
            columnClause(statement);
            leftJoinClause(statement);
            whereClause(statement, currentUser);
            //通过反射修改boundSql对象的sql语句
            ReflectUtils.setFieldValue(boundSql, "sql", statement.toString());
            logger.debug("modify sql: {}", statement.toString());

            //执行修改后的sql语句
//        return executor.query(mappedStatement, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            //执行结果
        } catch (Exception e) {
            e.printStackTrace();
        }
        return invocation.proceed();
    }

    private static void whereClause(Statement statement, SysUser currentUser) throws Exception {
        List<String> doneDataScope = new ArrayList<>();
        PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody();
        StringBuilder sqlStr = new StringBuilder();
        // 排序
        List<SysRole> roles = currentUser.getRoles().stream().sorted(Comparator.comparing(SysRole::getDataScope)).collect(Collectors.toList());
        for (SysRole role : roles) {
            String dataScope = role.getDataScope();
            if (doneDataScope.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE)) {
                continue;
            }
            if (DATA_SCOPE_ALL.equals(dataScope)) {
                doneDataScope.add(dataScope);
                break;
            } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
                // 多个自定数据权限使用in查询，避免多次拼接。
                sqlStr.append(StringUtils.format(" OR d.dept_id in ( SELECT dept_id FROM sys_role_dept WHERE role_id in ( select role_id from sys_user_role where user_id = {}) ) ", currentUser.getUserId()));
            } else if (DATA_SCOPE_DEPT.equals(dataScope)) {
                sqlStr.append(StringUtils.format(" OR d.dept_id = ( select u.dept_id from sys_user u  where u.user_id = {} ) ", currentUser.getUserId()));
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
                sqlStr.append(StringUtils.format(" OR d.dept_id in ( select dept_id from  sys_dept WHERE dept_id = (select dept_id from sys_user where user_id = {}) or find_in_set( (select dept_id from sys_user where user_id = {}),ancestors  ) ) ", currentUser.getUserId(), currentUser.getUserId()));
            } else if (DATA_SCOPE_SELF.equals(dataScope)) {
                sqlStr.append(StringUtils.format(" OR u.user_id = {} ", currentUser.getUserId()));
            }
            doneDataScope.add(dataScope);
        }
        String sqlString = "";
        if (StringUtils.isNotBlank(sqlStr.toString())) {
            sqlString = " (" + sqlStr.substring(4) + ")";
        }
        logger.debug("sql: {}", sqlString);
        Expression expression = CCJSqlParserUtil.parseExpression(sqlString);
        if (plainSelect.getWhere() != null) {
            Expression where = plainSelect.getWhere();
            AndExpression andExpression = new AndExpression();
            andExpression.setLeftExpression(where);
            andExpression.setRightExpression(expression);
        }
        plainSelect.setWhere(expression);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }


    private static void columnClause(Statement statement) throws Exception {
        PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody();
        String aliasName = "";
        Table table = (Table) plainSelect.getFromItem();
        String name = table.getName();
        aliasName = name.substring(0, 1);
        Alias alias = plainSelect.getFromItem().getAlias();
        if (alias == null) {
            alias = new Alias(aliasName);
            table.setAlias(alias);
        }
        for (SelectItem selectItem : plainSelect.getSelectItems()) {
            selectItem.accept(new SelectItemVisitorAdapter() {
                @Override
                public void visit(SelectExpressionItem selectExpressionItem) {
                    selectExpressionItem.getExpression().accept(new ExpressionVisitorAdapter() {
                        public void visit(Column column) {
                            column.setTable(table);
                        }
                    });
                }
            });
        }
    }

    private static void leftJoinClause(Statement statement) throws Exception {
        if (statement instanceof Select) {
            Select select = (Select) statement;
            PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
            String aliasName = "";
            Table table = (Table) plainSelect.getFromItem();
            String name = table.getName();
            aliasName = name.substring(0, 1);
            Alias alias = plainSelect.getFromItem().getAlias();
            if (alias == null) {
                alias = new Alias(aliasName);
                table.setAlias(alias);
            }
            Join joinUser = new Join();
            Table sysUserTable = new Table("sys_user").withAlias(new Alias("u", false));
            joinUser.withLeft(true).withRightItem(sysUserTable);
            EqualsTo equalsTo = new EqualsTo();
            equalsTo.withLeftExpression(new Column(sysUserTable, "user_id"));
            equalsTo.withRightExpression(new Column(table, "create_by_id"));
            joinUser.setOnExpressions(Collections.singleton(equalsTo));

            Join joinDept = new Join();
            Table sysDeptTable = new Table("sys_dept").withAlias(new Alias("d", false));
            joinDept.withLeft(true).withRightItem(sysDeptTable);
            EqualsTo equalsToDept = new EqualsTo();
            equalsToDept.withLeftExpression(new Column(sysDeptTable, "dept_id"));
            equalsToDept.withRightExpression(new Column(sysUserTable, "dept_id"));
            joinDept.setOnExpressions(Collections.singleton(equalsToDept));
            plainSelect.addJoins(joinUser, joinDept);
        }
    }
}

